/* Author: Anirudh Sasikumar */
/* URL: http://anirudhs.chaosnet.org/ */
/* Project URL: http://code.google.com/p/fxstruts/ */

/* FxXMLOutput is licensed under the LGPL. */

/* FxXMLOutput is free software: you can redistribute it */
/* and/or modify it under the terms of the GNU Lesser General */
/* Public License version 3.0 as published by the Free Software
 * Foundation. */

/* FxXMLOutput is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the */
/* GNU Lesser General Public License for more details. */

/* You should have received a copy of the GNU Lesser General */
/* Public License 3.0 along with FxStruts Taglib.  If not, see */
/* <http://www.gnu.org/licenses/>. */

/* This is based on blazeds AMF serialization code */

/* Warning: Currently this is *extremely* buggy. Here lie very 
 * very scary monsters!
 */
package org.fxstruts.xml;

import java.io.IOException;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.struts.taglib.TagUtils;
import org.w3c.dom.Document;

import flex.messaging.MessageException;
import flex.messaging.io.ArrayCollection;
import flex.messaging.io.PropertyProxy;
import flex.messaging.io.PropertyProxyRegistry;
//import flex.messaging.io.SerializationContext;
import flex.messaging.io.SerializationDescriptor;
//import flex.messaging.io.amf.Amf3Output;
import flex.messaging.util.XMLUtil;

public class FxXMLOutput 
{
    
    
    public FxXMLOutput()
    {
        
    }
    
    public String writeMap(Map map)
    {
        String res = "";
        Iterator it = map.keySet().iterator();
        while (it.hasNext())
        {
            Object key = it.next();
            if (key != null)
            {
                String propName = key.toString();
                res += "<" + propName + ">";
                res += writeObject(map.get(key));
                res += "</" + propName + ">";
            }
        }
        return res;
    }
    
    protected String writeArray(Object o, Class componentType, String parentName) throws IOException
    {
    	String res = "";
        if (componentType.isPrimitive())
        {
            res += writePrimitiveArray(o, parentName);
        }
        else if (componentType.equals(Byte.class))
        {
            res += writeByteArray((Byte[])o, parentName);
        }
        else if (componentType.equals(Character.class))
        {
            res += writeCharArrayAsString((Character[])o, parentName);
        }
        else
        {
            res += writeObjectArray((Object[])o, null, parentName);
        }
        return res;
    }
    
    protected String writePrimitiveArray(Object obj, String parentName) throws IOException
    {
        String res = "";
        Class aType = obj.getClass().getComponentType();
        String propName = normalizeClassName(obj.getClass().getName());
        if ( parentName != null )
        {
        	propName = parentName;
        }
        if (aType.equals(Character.TYPE))
        {
            //Treat char[] as a String
            char[] c = (char[])obj;
            res += writeCharArrayAsString(c, propName);
        }
        else if (aType.equals(Byte.TYPE))
        {
            res += writeByteArray((byte[])obj, propName);
        }
        else
        {
            if (aType.equals(Boolean.TYPE))
            {
                boolean[] b = (boolean[])obj;
                
                for (int i = 0; i < b.length; i++)
                {
                    res += "<" + propName + ">";
                    res += b[i] + "";
                    res += "</" + propName + ">";
                }
                
            }
            else if (aType.equals(Integer.TYPE) || aType.equals(Short.TYPE))
            {
                //We have a primitive number, either an int or short
                //We write all of these as Integers...
                int length = Array.getLength(obj);
                
                
                for (int i = 0; i < length; i++)
                {
                    int v = Array.getInt(obj, i);
                    res += "<" + propName + ">";
                    res += v + "";
                    res += "</" + propName + ">";
                }
                
            }
            else
            {
                //We have a primitive number, either a double, float, or long
                //We write all of these as doubles...
                int length = Array.getLength(obj);
                
                
                for (int i = 0; i < length; i++)
                {
                    double v = Array.getDouble(obj, i);
                    res += "<" + propName + ">";
                    res += v + "";
                    res += "</" + propName + ">";
                }
            }
            
        }
        return res;
    }
    
    /**
     * @exclude
     */
    protected String writeByteArray(byte[] ba, String parentName) throws IOException
    {
    	String res = "";
        int length = ba.length;
        
        for (int i = 0; i < length; i++)
        {
            	res += "<" + parentName + ">";
            	//TODO: Base 64 encode
                res += ba[i];                
                res += "</" + parentName + ">";

        }
        return res;
 
    }
    
    /**
     * @exclude
     */
    protected String writeByteArray(Byte[] ba, String parentName) throws IOException
    {
        String res = "";
        int length = ba.length;
        
        for (int i = 0; i < length; i++)
        {
            Byte b = ba[i];
            if (b == null)
                res += "0";
            else
            {
            	res += "<" + parentName + ">";
            	//TODO: Base 64 encode
                res += b.byteValue();                
                res += "</" + parentName + ">";
            }
        }
        return res;
    }
    
    /**
     * @exclude
     */
    protected String writeCharArrayAsString(Character[] ca, String parentName) throws IOException
    {
        int length = ca.length;
        char[] chars = new char[length];
        
        for (int i = 0; i < length; i++)
        {
            Character c = ca[i];
            if (c == null)
                chars[i] = 0;
            else
                chars[i] = ca[i].charValue();
        }
        return writeCharArrayAsString(chars, parentName);
    }
    
    /**
     * @exclude
     */
    protected String writeCharArrayAsString(char[] ca, String parentName) throws IOException
    {
        String str = new String();
        int length = ca.length;
        for ( int i = 0; i < length; i++ )
        {
        	str += "<" + parentName + ">";
        	str += ca[i];
        	str += "</" + parentName + ">";
        }
        return str;
    }
    
    private String normalizeClassName(String clsName)
    {
    	String res = clsName;
    	
    	if ( res.startsWith("[L" ))
    		res = res.substring(2);
    	else if ( res.startsWith("[") && res.length() > 1 )
    		res = res.substring(1);
    	if ( res.endsWith(";" ))
    		res = res.substring(0, res.length() - 1);
    	
    	int lastDot = -1;
    	if ( (lastDot = res.lastIndexOf('.')) != -1 )
    	{
    		res = res.substring(lastDot + 1, res.length());
    	}
    	
    	return res;
    }
    
    /**
     * @exclude
     */
    protected String writeObjectArray(Object[] values, SerializationDescriptor descriptor, String clsName) throws IOException
    {   
    	String res = "";
    	if ( clsName == null )
    	{
    		String tmpClsName = values.getClass().getName();
    		if ( tmpClsName == null || tmpClsName.length() == 0 )
    			clsName = "array";
    		else
    			clsName = normalizeClassName(tmpClsName);
    	}
    	//Anirudh: To make nested Arrays with more than one property in it correct, we are going to repeat the clsname for each item in the array
    	//res += "<" + clsName + ">";
        for (int i = 0; i < values.length; ++i)
        {           
            Object item = values[i];
            if (item != null && descriptor != null && !(item instanceof String) 
                && !(item instanceof Number) && !(item instanceof Boolean) 
                && !(item instanceof Character))
            {
                PropertyProxy proxy = PropertyProxyRegistry.getProxy(item);
                proxy = (PropertyProxy)proxy.clone();
                proxy.setDescriptor(descriptor);
                proxy.setDefaultInstance(item);
                item = proxy;
            }
            res += "<" + clsName + ">";
            res += writeObject(item, clsName);
            res += "</" + clsName + ">";
        }
        //res += "</" + clsName + ">";
        return res;
        
    }
    
    protected String writePropertyProxy(PropertyProxy proxy, Object instance, String clsName) throws IOException
    {
    	String res = "";
        /*
         * At this point we substitute the instance we want to serialize.
         */
        Object newInst = proxy.getInstanceToSerialize(instance);
        if (newInst != instance)
        {
            // We can't use writeAMFNull here I think since we already added this object
            // to the object table on the server side.  The player won't have any way
            // of knowing we have this reference mapped to null.
            if (newInst == null)
                throw new MessageException("PropertyProxy.getInstanceToSerialize class: " + proxy.getClass() + " returned null for instance class: " + instance.getClass().getName());
            
            // Grab a new proxy if necessary for the new instance
            proxy = PropertyProxyRegistry.getProxyAndRegister(newInst);
            instance = newInst;
        }
        
        List propertyNames = null;
        boolean externalizable = proxy.isExternalizable(instance);
        
        if (!externalizable)
            propertyNames = proxy.getPropertyNames(instance);
        
        //TraitsInfo ti = new TraitsInfo(proxy.getAlias(instance), proxy.isDynamic(), externalizable, propertyNames);
        //writeObjectTraits(ti);
        
//        if (externalizable)
//        {
//        	StringOutputStream strBuf = new StringOutputStream();
//        	ObjectOutputStream oBuf = new ObjectOutputStream(strBuf);
////            // Call user defined serialization
//            ((Externalizable)instance).writeExternal(oBuf);
//            res += strBuf.getString();
//        }
//        else 
        if (propertyNames != null)
        {
            Iterator it = propertyNames.iterator();
            // Anirudh: If you want the type name to be the parent tag, uncomment line below
            //res += "<" + clsName + ">";
            while (it.hasNext())
            {
                String propName = (String)it.next();
                Object value = null;
                value = proxy.getValue(instance, propName);
                
                //Anirudh: This may need to be commented
                //res += "<" + propName + ">";
                res += writeObject(value, propName);                
                //res += "</" + propName + ">";
            }
            //res += "</" + clsName + ">";
        }
        else if ( instance instanceof Collection )
        {
            Iterator it = ((Collection)instance).iterator();
            while (it.hasNext())
            {
                Object propName = it.next();
                
                
                res += "<" + clsName + ">";
                res += writeObject(propName);                
                res += "</" + clsName + ">";
            }
        }
        
        return res;
    }
    
    public String writeArrayCollection(Collection col, String clsName)
    {
    	String res = "";
        ArrayCollection ac;
        
        if (col instanceof ArrayCollection)
        {
            ac = (ArrayCollection)col; 
        }
        else
        {
            // Wrap any Collection in an ArrayCollection
            ac = new ArrayCollection(col);
        }        
        
        
        // Then wrap ArrayCollection in PropertyProxy for bean-like serialization
        PropertyProxy proxy = PropertyProxyRegistry.getProxy(ac);
        try
        {
        	if ( clsName == null )
        		clsName = "array";
            res += writePropertyProxy(proxy, ac, clsName);
        }
        catch (IOException e)
        {
            System.err.println("IOException in writeArrayCollection of FxXMLOutput");
        }
        
        return res;
        
    }
    
    
    protected String writeCustomObject(Object o, String parentName) throws IOException
    {
    	String res = "";
        PropertyProxy proxy = null;
        
        if (o instanceof PropertyProxy)
        {
            proxy = (PropertyProxy)o;
            o = proxy.getDefaultInstance();
            
            // The proxy may wrap a null default instance, if so, short circuit here.
            if (o == null)
            {
                
                return res;
            }
            
            // HACK: Short circuit and unwrap if PropertyProxy is wrapping an Array
            // or Collection or Map with legacyMap as true since we don't yet have 
            // the ability to proxy multiple AMF types. We write an AMF Array directly 
            // instead of an AMF Object...
            else if (o instanceof Collection)
            {
                res += writeArrayCollection((Collection)o, parentName);
                return res;
            }
            else if (o.getClass().isArray())
            {
                res += writeObjectArray((Object[])o, proxy.getDescriptor(), null);
                return res;
            }
            else if (o instanceof Map )
            {
                res += writeMap((Map)o);
                return res;
            }
        }        
        
        
        if (proxy == null)
        {
            proxy = PropertyProxyRegistry.getProxyAndRegister(o);
        }
        if ( parentName == null )
            res += writePropertyProxy(proxy, o, normalizeClassName(o.getClass().getName()));
        else
            res += writePropertyProxy(proxy, o, parentName);
        return res;
    }
    
    public String writeObject(Object value)
    {
        
    	return writeObject(value, null);
    }
    
    private String encapsulateVal(String val, String tag)
    {
    	if ( tag != null )
            return "<" + tag + ">" + TagUtils.getInstance().filter(val) + "</" + tag + ">";
    	else
            return TagUtils.getInstance().filter(val);
    }
    
    public String writeObject(Object value, String parentName)
    {
        String res = "";
        
        if (value == null)
        {
            return "";
        }
        
        if (value instanceof String || value instanceof Character)
        {
            res = encapsulateVal(value.toString(), parentName);
        }
        else if (value instanceof Number)
        {
            if (value instanceof Integer || value instanceof Short || value instanceof Byte)
            {
                int i = ((Number)value).intValue();
                res = encapsulateVal(i + "", parentName);
            }
            else if ((value instanceof BigInteger || value instanceof BigDecimal))
            {
                // Using double to write big numbers such as BigInteger or
                // BigDecimal can result in information loss so we write
                // them as String by default...
                res = encapsulateVal(((Number)value).toString(), parentName);
            }
            else
            {
                double d = ((Number)value).doubleValue();
                res =  encapsulateVal(d + "", parentName);
            }
        }
        else if (value instanceof Boolean)
        {
            res =  encapsulateVal((((Boolean)value).booleanValue()) + "", parentName);
        }
        // We have a complex type...
        else if (value instanceof Date)
        {
            //res =  encapsulateVal((double)((Date)value).getTime() + "", parentName);
        	res =  encapsulateVal(((Date)value).toString() + "", parentName);
        }
        else if (value instanceof Calendar)
        {
           // res =  encapsulateVal((double)(((Calendar)value).getTime().getTime()) + "", parentName);
        	 res =  encapsulateVal((((Calendar)value).getTime().toString()) + "", parentName);
        }
        else if (value instanceof Document)
        {
            try
            {
                res = encapsulateVal(XMLUtil.documentToString((Document)value), parentName);
            }
            catch (IOException io)
            {
                System.err.println("IOException in writeObject: " + io.getMessage());
            }
        }
        else
        {
            // We have an Object or Array type...
            Class cls = value.getClass();
            
            if (value instanceof Map)
            {
                res += writeMap((Map)value);
            }
            else if (value instanceof Collection)
            {
                res += writeArrayCollection((Collection)value, parentName);
            }
            else if (cls.isArray())
            {
            	try
            	{
                    res += writeArray(value, value.getClass().getComponentType(), parentName);
            	}
            	catch (IOException io1)
            	{
                    System.err.println("IOException in writeObject:" + io1.getMessage());
            	}
            }
            else
            {
                //Special Case: wrap RowSet in PageableRowSet for Serialization
//                if (o instanceof RowSet)
//                {
//                    o = new PagedRowSet((RowSet)o, Integer.MAX_VALUE, false);
//                }
//                else if (context.legacyThrowable && o instanceof Throwable)
//                {
//                    o = new StatusInfoProxy((Throwable)o);
//                }
            	try
            	{
                    res += writeCustomObject(value, parentName);
            	}
            	catch (IOException io2)
            	{
                    System.err.println("IOException in writeObject:" + io2.getMessage());
            	}
            }
        }
        
        return res;
    }
}
